package javango.forms;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javango.forms.fields.BoundField;
import javango.forms.fields.Field;
import javango.forms.fields.FieldFactory;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.inject.Inject;

public class BaseForm implements Form {
	public final static String NON_FIELD_ERRORS = "__all__";
		
	private final static Log log = LogFactory.getLog(BaseForm.class);
	
	protected Map<String, Field> _fields = new LinkedHashMap<String, Field>();  // use caution when using this directly, user getter (getFields());
	protected Map<String, String[]> data;
	protected Map<String, String> errors; // null until isValid is called
	protected Map<String, Object> cleanedData; // null until isValid is called
	protected Map<String, Object> initial;
	protected String prefix;
	protected String id = "id_%s"; // set to null to disable ID.
	protected boolean readOnly;
	
	protected boolean init = false;
	
	protected FieldFactory fieldFactory;
	@Inject
	public BaseForm(FieldFactory fieldFactory) {
		this.fieldFactory = fieldFactory;
	}
	
	public Form bind(Map<String, String[]> map) {
		this.data = map;
		return this;
	}
	
	/**
	 * Returns true if this is a bound form,  bound forms have been associated with input from a user.
	 * @return
	 */
	public boolean isBound() {
		return this.data != null;
	}
	
	/**
	 * If this form has a prefix, returns the field name with the form's prefix appended.
	 * @param fieldName
	 */
	protected String addPrefix(String fieldName) {
		return getPrefix() == null ? fieldName : String.format("%s-%s", getPrefix(), fieldName);
	}
	
	/**
	 * Cleans all of data and populates errors and cleanedData
	 */
	protected void fullClean() {
		if (cleanedData != null) return;  // we have already run full clean I don't think we should run again.
		errors = new HashMap<String, String>();
		if (!isBound()) return;
		cleanedData = new HashMap<String, Object>();

		for (Entry<String, Field> e : getFields().entrySet()) {			
			Field field = e.getValue();
			String name = field.getName() == null ? e.getKey() : field.getName();
			
			if (field.getName() == null) { // TODO This is here in case a field does not know its name..
				field.setName(name);
			}
			
			String[] value = field.getWidget().valueFromMap(data, addPrefix(name));
						
                       try {
                                cleanedData.put(name, field.clean(value, errors));

                                try {
                                        Method m = this.getClass().getMethod("clean_" + name);
                                        cleanedData.put(name, m.invoke(this));
                                } catch (NoSuchMethodException ex) {
                                        // ouch is this the best way to figure out that a clean_ method does not exist,  seems an exception might be too slow.
                                } catch (InvocationTargetException ex) { // TODO should this be logged
                                        LogFactory.getLog(BaseForm.class).error(ex,ex);
                                        if (ex.getCause() instanceof ValidationException) {
                                                throw (ValidationException)ex.getCause();
                                        }
                                        throw new RuntimeException(
                                                                String.format("Unable to run clean_%s method due to an InvocationTargetException", name));
                                } catch (IllegalAccessException ex) {
                                        LogFactory.getLog(BaseForm.class).error(ex,ex);
                                        throw new RuntimeException("Unable to run clean_%s method due to an IllegalAccessException");
                                }

                        } catch (ValidationException ex) {
                                errors.put(name, ex.getMessage());
                                cleanedData.remove(name);
                        }

		}
		
		try {
			clean();
		} catch (ValidationException ex) {
			errors.put(NON_FIELD_ERRORS, ex.getMessage()); // TODO allow more than one non-field error??
		}
		
		if (!errors.isEmpty()) {
			cleanedData = null;
		}
	}
	
	/**
	 * Hook for doing any additional form-wide cleaning after fullClean has been called,  at this point Field.clean will have
	 * been called for all fields.
	 */
	protected void clean() throws ValidationException {
		
	}
	
	/**
	 * Return true if this form is valid.
	 * @return
	 */
	public boolean isValid() {	
		return isBound() && getErrors().isEmpty();
	}
	
	/**
	 * Returns a Map of field errors
	 * TODO If we go with Hibernate validator, should this return a Map/List of hibernate validators
	 * @return
	 */
	public Map<String, String> getErrors() {
		if (errors == null) {
			fullClean();
		}
		return errors;
	}
	
	/**
	 * Cleans the data from this form into the specified object, returns null if the form is not valid.
	 * 
	 * @param object
	 */
	public Object clean(Object bean) {
		fullClean();
		if (!errors.isEmpty()) return null;
 
		for (Entry<String, Object> entry : getCleanedData().entrySet()) {
			String fieldName = entry.getKey();
			Field f = getFields().get(fieldName);			
			if (f == null || f.isEditable()) { // f maybe null if cleaned data holds a value that did not come directly from a field.
				if (log.isDebugEnabled()) log.debug(String.format("Cleaning : '$s' with value '%s'", fieldName, entry.getValue()));
                if (PropertyUtils.isWriteable(bean, fieldName)) {
                	try {
               			PropertyUtils.setProperty(bean, fieldName, entry.getValue());
                	} catch (InvocationTargetException e) {
                		log.error(e,e);
                	} catch (IllegalAccessException e) {
                		log.error(e,e);
                	} catch (NoSuchMethodException e) {
                		log.error(e,e);
                	}
                }
			}
	    }
		return bean;
	}
	
	/**
	 * Cleans the data in this form into a new bean of the specified class.
	 * @param objectClass
	 * @return
	 */
	public Object clean(Class objectClass) {
		try {
			Object bean = objectClass.newInstance();
			return clean(bean);
		} catch (IllegalAccessException e) {
			log.error(e,e);
		} catch (InstantiationException e) {
			log.error(e,e);
		}
		return null;
	}
	
	protected Field loadField(java.lang.reflect.Field field) {
		if (!Field.class.isAssignableFrom(field.getType())) {
			if (log.isDebugEnabled()) log.debug("Field not extendded from Field class, skipping: " + field.getName());
			return null;
		}
		if (_fields.containsKey(field.getName())) {
			if (log.isDebugEnabled()) log.debug("Field already in field list, skipping: " + field.getName());
			return null;			
		}
		
		try {
			Field baseField = (Field)field.get(this);
			if (baseField == null) {
				baseField = fieldFactory.newField((Class<? extends Field>)field.getType());
				field.set(this, baseField);
			}
			
			baseField.setName(field.getName());
			_fields.put(field.getName(), baseField);
			
			Annotation[] annotations = field.getAnnotations();
			for (Annotation annotation: annotations) {
					baseField.handleAnnotation(annotation);
			}
			
			return baseField;
		} catch (IllegalAccessException e) {
			log.error("Unable to load field into form,  change visiblity to public: " + e,e);
			return null;
		}		
	}
	protected void init() {
		if (init) return; // no reason to call twice!!
		
		Class cls = this.getClass();
		java.lang.reflect.Field[] classFields = cls.getDeclaredFields();
		for (int i=0; i<classFields.length; i++) {
			loadField(classFields[i]);
		}
		init = true;
	}
	
	public String asTable() {
		StringBuilder b = new StringBuilder();
		StringBuilder hidden_fields = new StringBuilder();
		
		for (Entry<String, Field> entry : getFields().entrySet()) {
			Field field = entry.getValue();
			String fieldName = field.getName() == null ? entry.getKey() : field.getName();
			BoundField bf = new BoundField(field, this, fieldName);
			
			if (bf.isHidden()) {
				// TODO Do something a little more correct with the hidden_fields,  like place just after the last field (ie in the same element).
				hidden_fields.append(bf.toString());
				hidden_fields.append("\n");
			} else {
				StringBuilder errors = new StringBuilder();
				if (this.getErrors().get(fieldName) != null) {
					errors.append("<ul class=\"errorlist\">");
					errors.append("<li>");
					errors.append(this.getErrors().get(fieldName)); // when this goes to a list,  use bf.geterorrs instead of this.
					errors.append("</li>");
					errors.append("</ul>");
				}
				b.append(String.format("<tr><th>%s</th><td>%s%s%s</td></tr>\n", bf.getVerboseName(), errors.toString(), bf.toString(), bf.getHelpText()));
			}
		}
		if (hidden_fields.length() ==0 ) {
			return b.toString();
		}
		if (b.length() > 11) {
			return b.insert(b.length()-11, hidden_fields).toString();
		}
		// humm,  how did we get here,  must only be hidden fields.... or not fields at all,  render them both
		return b.append(hidden_fields).toString();
	}
	
	public Map<String, Object> getCleanedData() {
		return cleanedData;
	}
	public Map<String, Field> getFields() {
		if (!init) init();		
		return _fields;
	}

	public Map<String, Object> getInitial() {
		if (initial == null) {
			initial = new HashMap<String, Object>();
		}
		return initial;
	}

	/**
	 * Set this form's initial values to the cooresponding fields in the provided bean.
	 * @param bean
	 * @return
	 */
	public Form setInitial(Object bean) {
		try {
			if (this.initial == null) this.initial = new HashMap<String, Object>();
			for (Entry<String, Field> e : getFields().entrySet()) {
				if (PropertyUtils.isReadable(bean, e.getKey())) {
					this.initial.put(e.getKey(), PropertyUtils.getProperty(bean, e.getKey()));
				}
			}
			// TODO what shoudl really be done on these exceptions...  continue with the rest,  throw something
		} catch (IllegalAccessException e) {
			log.error(e,e);
		} catch (NoSuchMethodException e) {
			log.error(e,e);
		} catch (InvocationTargetException e) {
			log.error(e,e);
		}
		return this;
	}
	
	public Form setInitial(Map<String, Object> initial) {
		this.initial = initial;
		return this;
	}
	
	public Map<String, String[]> getData() {
		return data;
	}
	public String getPrefix() {
		return prefix;
	}
	public Form setPrefix(String prefix) {
		this.prefix = prefix;
		return this;
	}
	
	/**
	 * Returns a bound field for the requested field.
	 * @param field
	 * @return
	 */
	public BoundField get(String field) {		
		if (!getFields().containsKey(field)) {
			return null;
		}
		
		Field f = getFields().get(field);
		String fieldName = f.getName() == null ? field : f.getName();
		return new BoundField(f, this, fieldName);
	}
	
	/**
	 * Returns an iterator of the BoundFields in the form.
	 */
	public Iterator<BoundField> iterator() {
		List<BoundField> boundFields = new ArrayList<BoundField>();
		for (Entry<String, Field> e : getFields().entrySet()) {
			Field field = e.getValue();
			String fieldName = field.getName() == null ? e.getKey() : field.getName();
			BoundField bf = new BoundField(field, this, fieldName);

			boundFields.add(bf);
		}
		return boundFields.iterator();
	}

	public String getId() {
		return id;
	}

	public BaseForm setId(String id) {
		this.id = id;
		return this;
	}

	public List<String> getNonFieldErrors() {
		List<String> nonFieldErrorList = new ArrayList<String>();
		if (errors != null && errors.containsKey(NON_FIELD_ERRORS)) {
			nonFieldErrorList.add(errors.get(NON_FIELD_ERRORS));
		}
		return nonFieldErrorList;
	}
	
        public Form setReadOnly(boolean readOnly) {
		this.readOnly = readOnly;
		return this;
	}

        public boolean isReadOnly() {
		return this.readOnly;
	}

	
}
